﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.Configuration;
using System.Security.Cryptography.X509Certificates;
using System.Reflection;

using System.Globalization;
using System.Threading;

using SsrsRenderStudio.Wcf;
using SsrsRenderStudio.Shared;

namespace SsrsRenderStudio.Wcf.Client
{
    // client config: http://msdn.microsoft.com/en-us/library/ms734663.aspx

    public class CustomClientFactory<T> : ChannelFactory<T>
    {
        public bool ClientConfiguredSuccessfully = false;

        /// <summary>
        /// Wildcard.
        /// </summary>
        public CustomClientFactory()
            : base(typeof(T))
        {
            base.InitializeEndpoint("*", null);
        }
        
        /// <summary>
        /// Named configuration.
        /// </summary>
        public CustomClientFactory(string endpointConfigurationName) : base(typeof(T))
        {
            base.InitializeEndpoint(endpointConfigurationName, null);
        }

        protected override void ApplyConfiguration(string configurationName)
        {
            string endpointConfigFile = string.Empty;
            
            try
            {
                WcfConfiguration.FetchNextBindingConfiguration(typeof(T), out endpointConfigFile);
                if (string.IsNullOrEmpty(endpointConfigFile)) return ;
            }
            catch (Exception ex)
            {
                Logger.Log(EnumLoggingSeverity.ERROR, "CustomClientFactory: " + ex.Message);
                return;
            }

            ClientConfiguredSuccessfully = CustomChannelFactoryHelper.ApplyConfiguration(endpointConfigFile, Endpoint, configurationName);
        }
    }

    static class CustomChannelFactoryHelper
    {
        private static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
        private static Dictionary<string, ServiceModelSectionGroup> groups = new Dictionary<string, ServiceModelSectionGroup>();

        public static bool ApplyConfiguration(string configurationFileName, ServiceEndpoint serviceEndpoint, string configurationName)
        {
            if (string.IsNullOrEmpty(configurationFileName))
                return false;

            ServiceModelSectionGroup serviceModeGroup = GetGroup(configurationFileName);
            LoadChannelBehaviors(serviceEndpoint, configurationName, serviceModeGroup);

            return true;
        }

        /// <summary>
        /// Load the endpoint with the binding, address, behaviour etc. from the alternative config file 
        /// </summary>
        /// <param name="serviceEndpoint"></param>
        /// <param name="configurationName"></param>
        /// <param name="serviceModeGroup"></param>
        /// <returns></returns>
        public static ServiceEndpoint LoadChannelBehaviors(ServiceEndpoint serviceEndpoint, string configurationName, ServiceModelSectionGroup serviceModeGroup)
        {
            bool isWildcard = string.Equals(configurationName, "*", StringComparison.Ordinal);
            ChannelEndpointElement provider = LookupChannel(serviceModeGroup, configurationName, serviceEndpoint.Contract.ConfigurationName, isWildcard);

            if (provider == null)
                return null;

            if (serviceEndpoint.Binding == null)
            {
                serviceEndpoint.Binding = LookupBinding(serviceModeGroup, provider.Binding, provider.BindingConfiguration);
            }

            if (serviceEndpoint.Address == null)
            {
                serviceEndpoint.Address = new EndpointAddress(provider.Address, LookupIdentity(provider.Identity), provider.Headers.Headers);
            }

            if (serviceEndpoint.Behaviors.Count == 0 && !String.IsNullOrEmpty(provider.BehaviorConfiguration))
            {
                LoadBehaviors(serviceModeGroup, provider.BehaviorConfiguration, serviceEndpoint);
            }

            serviceEndpoint.Name = provider.Contract;

            return serviceEndpoint;
        }

        /// <summary>
        /// Load the ServiceModel section fron the config file
        /// </summary>
        /// <param name="configurationFileName"></param>
        /// <returns></returns>
        private static ServiceModelSectionGroup GetGroup(string configurationFileName)
        {
            ServiceModelSectionGroup group;

            // Get a read lock while we access the cache collection
            readerWriterLock.AcquireReaderLock(-1);
            try
            {
                // Check to see if we already have a group for the given configuration
                if (groups.TryGetValue(configurationFileName, out group))
                {
                    // We found group so return it and we are done
                    return group;
                }
            }
            finally
            {
                // always release the lock safely
                readerWriterLock.ReleaseReaderLock();
            }

            // if we get here, we couldn't get a group so we need to create one
            // this will involve modifying the collection so we need a write lock
            readerWriterLock.AcquireWriterLock(-1);
            try
            {
                // check an open group wasn't created on another thread while we were
                // acquiring the writer lock
                if (groups.TryGetValue(configurationFileName, out group))
                {
                    // we found a group so return it and we are done
                    return group;
                }

                ExeConfigurationFileMap executionFileMap = new ExeConfigurationFileMap
                {
                    ExeConfigFilename = configurationFileName
                };

                System.Configuration.Configuration config = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(executionFileMap, ConfigurationUserLevel.None);
                group = ServiceModelSectionGroup.GetSectionGroup(config);

                // store it in the cache
                groups.Add(configurationFileName, group);

                return group;
            }
            finally
            {
                // always release the writer lock!
                readerWriterLock.ReleaseWriterLock();
            }
        }

        /// <summary>
        /// Find the endpoint in the alternative config file that matches the required contract and configuration
        /// </summary>
        /// <param name="serviceModeGroup"></param>
        /// <param name="configurationName"></param>
        /// <param name="contractName"></param>
        /// <param name="wildcard"></param>
        /// <returns></returns>
        private static ChannelEndpointElement LookupChannel(ServiceModelSectionGroup serviceModeGroup, string configurationName, string contractName, bool wildcard)
        {
            foreach (ChannelEndpointElement endpoint in serviceModeGroup.Client.Endpoints)
            {
                if (endpoint.Contract == contractName && (endpoint.Name == configurationName || wildcard))
                {
                    return endpoint;
                }
            }
            return null;
        }

        /// <summary>
        /// Configures the binding for the selected endpoint
        /// </summary>
        /// <param name="group"></param>
        /// <param name="bindingName"></param>
        /// <param name="configurationName"></param>
        /// <returns></returns>
        private static Binding LookupBinding(ServiceModelSectionGroup group, string bindingName, string configurationName)
        {
            BindingCollectionElement bindingElementCollection = group.Bindings[bindingName];
            if (bindingElementCollection.ConfiguredBindings.Count == 0)
                return null;

            IBindingConfigurationElement bindingConfigurationElement = bindingElementCollection.ConfiguredBindings.First(item => item.Name == configurationName);

            Binding binding = GetBinding(bindingConfigurationElement);
            if (bindingConfigurationElement != null)
            {
                bindingConfigurationElement.ApplyConfiguration(binding);
            }

            return binding;
        }

        /// <summary>
        /// Gets the endpoint identity from the configuration file
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        private static EndpointIdentity LookupIdentity(IdentityElement element)
        {
            EndpointIdentity identity = null;
            PropertyInformationCollection properties = element.ElementInformation.Properties;
            if (properties["userPrincipalName"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateUpnIdentity(element.UserPrincipalName.Value);
            }
            if (properties["servicePrincipalName"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateSpnIdentity(element.ServicePrincipalName.Value);
            }
            if (properties["dns"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateDnsIdentity(element.Dns.Value);
            }
            if (properties["rsa"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateRsaIdentity(element.Rsa.Value);
            }
            if (properties["certificate"].ValueOrigin != PropertyValueOrigin.Default)
            {
                X509Certificate2Collection supportingCertificates = new X509Certificate2Collection();
                supportingCertificates.Import(Convert.FromBase64String(element.Certificate.EncodedValue));
                if (supportingCertificates.Count == 0)
                {
                    throw new InvalidOperationException("UnableToLoadCertificateIdentity");
                }
                X509Certificate2 primaryCertificate = supportingCertificates[0];
                supportingCertificates.RemoveAt(0);
                return EndpointIdentity.CreateX509CertificateIdentity(primaryCertificate, supportingCertificates);
            }

            return identity;
        }

        /// <summary>
        /// Adds the configured behavior to the selected endpoint
        /// </summary>
        /// <param name="group"></param>
        /// <param name="behaviorConfiguration"></param>
        /// <param name="serviceEndpoint"></param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Type.InvokeMember", Justification = "This is a real hack, but there is no other way of doing it :(")]
        private static void LoadBehaviors(ServiceModelSectionGroup group, string behaviorConfiguration, ServiceEndpoint serviceEndpoint)
        {
            EndpointBehaviorElement behaviorElement = group.Behaviors.EndpointBehaviors[behaviorConfiguration];
            for (int i = 0; i < behaviorElement.Count; i++)
            {
                BehaviorExtensionElement behaviorExtension = behaviorElement[i];
                object extension = behaviorExtension.GetType().InvokeMember
                 (
                 "CreateBehavior",
                 BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance,
                 null,
                 behaviorExtension,
                 null,
                 CultureInfo.InvariantCulture
                 );
                if (extension != null)
                {
                    serviceEndpoint.Behaviors.Add((IEndpointBehavior)extension);
                }
            }
        }

        /// <summary>
        /// Helper method to create the right binding depending on the configuration element
        /// </summary>
        /// <param name="configurationElement"></param>
        /// <returns></returns>
        private static Binding GetBinding(IBindingConfigurationElement configurationElement)
        {
            if (configurationElement is NetTcpBindingElement)
                return new NetTcpBinding();

            if (configurationElement is NetMsmqBindingElement)
                return new NetMsmqBinding();

            if (configurationElement is BasicHttpBindingElement)
                return new BasicHttpBinding();

            if (configurationElement is NetNamedPipeBindingElement)
                return new NetNamedPipeBinding();

            if (configurationElement is NetPeerTcpBindingElement)
                return new NetPeerTcpBinding();

            if (configurationElement is WSDualHttpBindingElement)
                return new WSDualHttpBinding();

            if (configurationElement is WSHttpBindingElement)
                return new WSHttpBinding();

            if (configurationElement is WSFederationHttpBindingElement)
                return new WSFederationHttpBinding();

            if (configurationElement is CustomBindingElement)
                return new CustomBinding();

            return null;
        }
    }

    
}
